package org.cerberus.platform.internal.impl;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.cerberus.platform.api.PAttribute;
import org.cerberus.platform.api.PComponent;
import org.cerberus.platform.internal.IPlatformClassLoader;
import org.cerberus.platform.internal.domain.IComponentClass;
import org.cerberus.platform.internal.domain.IComponentClassAttribute;
import org.cerberus.platform.internal.domain.IComponentClassEvent;

/**
 * Implementation of {@link IPlatformClassLoader}. See interface documentation
 * for further details.
 * 
 * @see ComponentClass
 * @see ComponentClassAttribute
 * @see ComponentClassEvent
 * @see ComponentClassResource
 * 
 * TODO getter style attributes are currently not supported
 * TODO implement events
 * TODO implement resources
 * TODO performance improvements for decrement()
 * 
 * @author Christian Humer
 * @since 2.0
 */
class PlatformClassLoader implements IPlatformClassLoader {

	/** The expected size of classes */
	private static final int INITIAL_SIZE = 200;
	
	/** Optimization constant*/
	private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class<?>[0];
	
	Map<Class<?>, IComponentClass> classMap;
	Set<String> classIDs;
	
	// temporary holder for the load callback
	LoaderCallback currentCallback;
	
	public PlatformClassLoader() {
		classMap = new HashMap<Class<?>, IComponentClass>(INITIAL_SIZE);
		classIDs = new HashSet<String>(INITIAL_SIZE);
	}
	
	public IComponentClass defineClass(Class<?> clazz, LoaderCallback callback) {
		currentCallback = callback;
		IComponentClass componentClass = classMap.get(clazz);
		if (componentClass == null) {
			componentClass = defineClass(clazz);
		}
		currentCallback = null;
		return componentClass;
	}
	
	public void increment(IComponentClass clazz, LoaderCallback callback) {
		if (clazz.isDeclared() && !clazz.isPrimitive()) {
			((ComponentClass)clazz).instanceCount++;
		}
	}
	
	public void decrement(IComponentClass clazz, UnLoaderCallback callback) {
		if (clazz.isDeclared() && !clazz.isPrimitive()) {
			((ComponentClass)clazz).instanceCount--;
			if (((ComponentClass)clazz).instanceCount <= 0)  {
				verifyReferences(clazz, callback, new HashSet<IComponentClass>());
			}
		}
	}
	
	private void verifyReferences(IComponentClass clazz, UnLoaderCallback callback, Set<IComponentClass> verified) {
		if (clazz.isPrimitive() || !clazz.isDeclared()) {
			return;
		}
		// necessary for cycle detection
		if (verified.contains(clazz)) {
			return;
		}
		
		Set<IComponentClass> referenced = findReferenced(clazz, null);
		
		// check if one of the referenced has an instance count
		// -> can't be freed
		for (IComponentClass componentClass : referenced) {
			if (componentClass.isDeclared() && !componentClass.isPrimitive()) {
				if (((ComponentClass)componentClass).instanceCount > 0) {
					return;
				}
			}
		}
		
		// if all references are not referenced any more
		// we can search for all referencing classes and try to free them
		Set<IComponentClass> referencing = findReferencing(clazz);
		
		// all referencing should have instance count 0
		for (IComponentClass componentClass : referencing) {
			if (componentClass.isDeclared() && !componentClass.isPrimitive()) {
				if (((ComponentClass)componentClass).instanceCount > 0) {
					return;
				}
			}
		}
		
		
		removeClass(findJavaClass(clazz), clazz);
		callback.unloaded(clazz);
		
		// add to verified for cycle detection
		verified.add(clazz);
		
		// now check references and referencing for freeable
		// classes.
		for (IComponentClass componentClass : referencing) {
			if (componentClass.isDeclared() && !componentClass.isPrimitive()) {
				if (((ComponentClass) componentClass).instanceCount <= 0) {
					verifyReferences(componentClass, callback, verified);
				}
			}
		}
		for (IComponentClass componentClass : referenced) {
			if (componentClass.isDeclared() && !componentClass.isPrimitive()) {
				if (((ComponentClass) componentClass).instanceCount <= 0) {
					verifyReferences(componentClass, callback, verified);
				}
			}
		}
		
	}
	
	private Class<?> findJavaClass(IComponentClass searchClass) {
		for (Class<?> clazz : classMap.keySet()) {
			IComponentClass componentClazz = classMap.get(clazz);
			if (componentClazz == searchClass) {
				return clazz;
			}
		}
		return null;
	}
	
	private Set<IComponentClass> findReferencing(IComponentClass clazz) {
		Set<IComponentClass> referencing = new HashSet<IComponentClass>();
		for (IComponentClass componentClass : classMap.values()) {
			
			// itself should not be checked for referencing
			if (componentClass == clazz) {
				continue;
			}
			
			IComponentClass superClass = componentClass;
			while ((superClass = superClass.getSuperClass()) != null) {
				if (superClass == clazz) {
					referencing.add(componentClass);
					break;
				}
			}
			if (referencing.contains(componentClass)) {
				continue;
			}
			
			IComponentClassAttribute[] attributes = componentClass.getAttributes();
			if (attributes != null) {
				for (IComponentClassAttribute componentClassAttribute : attributes) {
					if (componentClassAttribute.getAttributeClass() == clazz) {
						referencing.add(componentClass);
						break;
					}
				}
			}
			
			if (referencing.contains(componentClass)) {
				continue;
			}
			
			IComponentClassEvent[] events = clazz.getEvents();
			if (events != null) {
				for (IComponentClassEvent componentClassEvent : events) {
					IComponentClass[] parameters = componentClassEvent.getParameters();
					if (parameters != null) {
						for (IComponentClass componentClass2 : parameters) {
							if (componentClass2 == clazz) {
								referencing.add(componentClass);
								break;
							}
						}
					}
					if (componentClassEvent.getReturnType() == clazz) {
						referencing.add(componentClass);
						break;
					}
					if (referencing.contains(componentClass)) {
						break;
					}
				}
			}
		}
		return referencing;
	}
	
	private Set<IComponentClass> findReferenced(IComponentClass clazz, Set<IComponentClass> list) {
		if (list == null) {
			list = new HashSet<IComponentClass>();
		}
		
		IComponentClass superClass = clazz;
		while ((superClass = superClass.getSuperClass()) != null) {
			if (list.add(superClass)) {
				list.addAll(findReferenced(superClass, list));
			}
		}
		IComponentClassAttribute[] attributes = clazz.getAttributes();
		if (attributes != null) {
			for (IComponentClassAttribute componentClassAttribute : attributes) {
				if (list.add(componentClassAttribute.getAttributeClass())) {
					list.addAll(findReferenced(componentClassAttribute.getAttributeClass(), list));
				}
			}			
		}
		IComponentClassEvent[] events = clazz.getEvents();
		if (events != null) {
			for (IComponentClassEvent event : events) {
				if (event.getParameters() != null) {
					for (IComponentClass parameter : event.getParameters()) {
						if ( list.add(parameter)) {
							list.addAll(findReferenced(parameter, list));
						}
					}					
				}
				if (event.getReturnType() != null) {
					if (list.add(event.getReturnType())) {
						list.addAll(findReferenced(event.getReturnType(), list));
					}
				}
			}			
		}
		return list;
	}
	
	@Override
	public boolean isLoaded(Class<?> clazz) {
		return classMap.containsKey(clazz);
	}
	
	private void notifyLoaded(IComponentClass clazz) {
		currentCallback.loaded(clazz);
	}
	
	private void removeClass(Class<?> javaClass, IComponentClass clazz) {
		classMap.remove(javaClass);
		classIDs.remove(clazz.getClassID());
	}
	
	private void addClass(Class<?> javaClass, IComponentClass clazz) {
		classMap.put(javaClass, clazz);
		classIDs.add(clazz.getClassID());
	}
	
	protected IComponentClass defineClass(Class<? extends Object> objectClass) {
		if (classMap.containsKey(objectClass)) {
			return classMap.get(objectClass);
		}
		
		PComponent classAnnotation = objectClass.getAnnotation(PComponent.class);
		if (classAnnotation == null) {
			// check if class is a native type
			IComponentClass nativeClass = SystemClasses.get(objectClass);
			if (nativeClass != null) {
				if (nativeClass.isDeclared()) {
					addClass(objectClass, nativeClass);
					notifyLoaded(nativeClass);
				}
				return nativeClass;
			}
		}
		String classID = classAnnotation.classID();
		if (classIDs.contains(classID)) {
			
			// find java class
			String foundJavaClassName = null;
			for (Class<?> clazz : classMap.keySet()) {
				IComponentClass componentClazz = classMap.get(clazz);
				if (componentClazz.getClassID().equals(classID)) {
					foundJavaClassName = clazz.getName();
					break;
				}
			}
			throw new ComponentClassException("ClassID '"+classID+"' conflict: " +
					"Class " +objectClass.getName()+" with "+foundJavaClassName);
		}
		boolean root = classAnnotation.root();
		boolean defaultModal = classAnnotation.modal();
		Class<? extends Object> superClass = objectClass.getSuperclass();
		ComponentClass superClassClass = null;
		
		if (superClass != null && superClass != Object.class) {
			defineClass(superClass);
			superClassClass = (ComponentClass)classMap.get(superClass);
		}
		ComponentClass clazz = new ComponentClass(superClassClass, 
				objectClass, classID, root, defaultModal);
		/*
		 * Its important to add a class to internal map before attributes are
		 * defined/resolved, because class can reference itself in an attribute.
		 */
		addClass(objectClass, clazz);
		
		boolean elementsDefined = false;
		try {
			clazz.defineAttributes(defineAttributes(objectClass));
			elementsDefined = true;
		} finally {
			/*
			 * We need to ensure that class is not added
			 * to map if elements are not valid.
			 */
			if (!elementsDefined) {
				removeClass(objectClass, clazz);
			}
		}
		
		notifyLoaded(clazz);
		
		return clazz;
	}
	
	protected IComponentClassAttribute[] defineAttributes(Class<?> clazz) {
		List<IComponentClassAttribute> list = new ArrayList<IComponentClassAttribute>();
		Field[] fields = clazz.getDeclaredFields();
		for (int i = 0; i < fields.length; i++) {
			Field field = fields[i];
			PAttribute attribute = field.getAnnotation(PAttribute.class);
			if (attribute != null) {
				String annotationName = attribute.name();
				if (annotationName.equals("")) {
					annotationName = field.getName();
				}
				Class<?> fieldType = field.getType();
				
				IComponentClass attributeClass = defineClass(fieldType);
				
				// attributeClass is never null
				
				ComponentClassAttribute classAttribute = new ComponentClassAttribute(
						annotationName, 
						attributeClass,
						fieldType,
						resolveGetter(clazz, field.getName(), fieldType),
						resolveSetter(clazz, field.getName(), fieldType));
				list.add(classAttribute);
			}
		}
		
		//TODO resolve getter style usage of PAttribute
		if (list.size() == 0) {
			return null;
		}
		return list.toArray(new IComponentClassAttribute[list.size()]);
	}
	
	protected Method resolveGetter(Class<? extends Object> clazz, String fieldName, Class<?> attributeType) {
		Method foundMethod = null;
		String methodSuffix = Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1, fieldName.length());
		String methodName = "get" + methodSuffix;
		try {
			foundMethod = clazz.getMethod(methodName, EMPTY_CLASS_ARRAY);
		} catch (Exception e) {
		}
		if (attributeType == boolean.class || attributeType == Boolean.class) {
			String booleanMethodName = "is" + methodSuffix;
			try {
				foundMethod = clazz.getMethod(booleanMethodName, EMPTY_CLASS_ARRAY);
			} catch (Exception e) {
			}
		}
		
		if (foundMethod == null) {
			throw new ComponentClassException("Could find a valid getter for attribute field "+fieldName+" in class "+clazz.getName()+" with returning type "+attributeType);
		}
		if (!foundMethod.getReturnType().equals(attributeType)) {
			throw new ComponentClassException("Found getter for attribute field "+fieldName+" in class "+clazz.getName()+" does not return the same type as the attribute definition "+attributeType.getName());
		}
		return foundMethod;
	}
	
	protected Method resolveSetter(Class<? extends Object> clazz, String fieldName, Class<?> attributeType) {
		String methodSuffix = Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1, fieldName.length());
		try {
			return clazz.getMethod("set"+methodSuffix, new Class<?>[]{attributeType});
		} catch (Exception e) {
		}
		return null;
	}
	
}
